From 4d225d3bb2e850bc0662f792ce202ba81780e3be Mon Sep 17 00:00:00 2001 From: =?utf8?q?IOhannes=20m=20zm=C3=B6lnig=20=28Debian/GNU=29?= Date: Thu, 5 Mar 2026 08:32:06 +0100 Subject: [PATCH] New upstream version 2.7.2+ds --- docs/Build/Linux.md | 2 +- docs/Documentation/NetworkProtocol.md | 246 ++++++++++++++++++++++++++ docs/changelog.yml | 11 ++ linux/Dockerfile.build | 5 + linux/README.md | 2 +- meson.build | 1 + mkdocs.yml | 1 + src/JackAudioInterface.cpp | 29 +++ src/OscServer.cpp | 2 +- src/Regulator.cpp | 80 +++++---- src/Regulator.h | 4 +- src/UdpHubListener.cpp | 3 +- src/jacktrip_globals.h | 2 +- src/main.cpp | 14 ++ src/vs/virtualstudio.cpp | 2 +- 15 files changed, 359 insertions(+), 45 deletions(-) create mode 100644 docs/Documentation/NetworkProtocol.md diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 144624f..87e36a8 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -43,7 +43,7 @@ apt install qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt ### Ubuntu and Debian/Raspbian (Qt6) ```sh apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man libclang-dev libdbus-1-dev libdbus-1-dev python3-jinja2 -apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window +apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libqt6core5compat6-dev libqt6shadertools6-dev libgl1-mesa-dev # for GUI builds apt install libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev diff --git a/docs/Documentation/NetworkProtocol.md b/docs/Documentation/NetworkProtocol.md new file mode 100644 index 0000000..fefba86 --- /dev/null +++ b/docs/Documentation/NetworkProtocol.md @@ -0,0 +1,246 @@ +## JackTrip network protocol (as implemented) + +This document describes JackTrip’s **on-the-wire protocol** as implemented in the current source tree. It is intended for developers debugging or interoperating with JackTrip at the packet level. + +### Scope and non-goals + +- **In scope**: the real-time **UDP audio stream**, its headers and payload layout, the optional **UDP redundancy** framing, the small **UDP “stop” control packet**, and the **TCP handshake** used by hub/ping-server style deployments (including the authentication variant). +- **Out of scope**: local-only IPC (e.g. `QLocalSocket` “AudioSocket”), OSC control, and any higher-level application semantics outside packet exchange. + +### Transports at a glance + +- **UDP (audio)**: real-time audio is sent as UDP datagrams containing `PacketHeader` + raw audio payload. +- **UDP (control)**: a small fixed-size “stop” datagram is used to signal shutdown. +- **TCP (hub/ping-server handshake)**: a short-lived TCP connection is used to exchange ephemeral UDP port information (and optionally do TLS + credentials). The client sends 4 bytes representing the port number it is binding to, and the server responds by sending 4 bytes representing its own port number. + +--- + +## UDP audio datagrams + +### High-level framing + +Each UDP datagram carries one of: + +- **Audio datagram**: one or more **full packets** (header + audio payload). When redundancy is disabled, there is exactly one full packet per UDP datagram. When redundancy is enabled, multiple full packets are concatenated into a single UDP datagram to provide forward error correction (FEC) (see “UDP redundancy”). +- **Stop/control datagram**: exactly 63 bytes of `0xFF` (see “UDP stop/control datagram”). + +### Packet header types + +The header is selected by `DataProtocol::packetHeaderTypeT`: + +- **DEFAULT**: `DefaultHeaderStruct` (the standard JackTrip header). +- **JAMLINK**: `JamLinkHeaderStuct` (JamLink compatibility). +- **EMPTY**: no header (payload only). + +See `src/PacketHeader.h` and `src/PacketHeader.cpp`. + +### Default header (`DEFAULT`) + +On-wire layout is the in-memory `DefaultHeaderStruct` copied with `memcpy()` (no explicit endian conversions). + +Fields (in order): + +| Field | Type | Meaning | +|------:|------|---------| +| `TimeStamp` | `uint64_t` | Timestamp in microseconds since Unix epoch (see `PacketHeader::usecTime()`). | +| `SeqNumber` | `uint16_t` | Sequence number; increments once per audio period and wraps at 16 bits. | +| `BufferSize` | `uint16_t` | Audio period size \(N\) in **samples per channel**. | +| `SamplingRate` | `uint8_t` | Encoded sample-rate enum value (`AudioInterface::samplingRateT`), **not** Hz. | +| `BitResolution` | `uint8_t` | Bits per sample (8/16/24/32). | +| `NumIncomingChannelsFromNet` | `uint8_t` | Channel count expected from the peer “from network” direction (see notes below). | +| `NumOutgoingChannelsToNet` | `uint8_t` | Channel count the sender is placing into the payload (see notes below). | + +#### Important interoperability notes + +- **Endianness / ABI**: this header is serialized by raw `memcpy()` of a C struct. In practice this assumes: + - both sides are using compatible ABI/layout for the struct, and + - both sides are on the same endianness (typically **little-endian** on modern desktop platforms). +- **Channel fields are asymmetric**: the implementation uses these fields to convey “incoming vs outgoing” channel counts, including a couple of sentinel behaviors: + - `NumIncomingChannelsFromNet` is populated from local *audio interface output* channel count. + - `NumOutgoingChannelsToNet` may be set to `0` when in/out channel counts match, or to `0xFF` when there are zero audio interface input channels. + +These behaviors come from `DefaultHeader::fillHeaderCommonFromAudio()` in `src/PacketHeader.cpp`. + +### JamLink header (`JAMLINK`) + +Please note that JamLink is an obsolete device. + +JamLink uses a compact header: + +| Field | Type | Meaning | +|------:|------|---------| +| `Common` | `uint16_t` | Bitfield describing mono/stereo, bit depth, sample rate, and samples-per-packet (JamLink “streamType”). | +| `SeqNumber` | `uint16_t` | Sequence number. | +| `TimeStamp` | `uint32_t` | Timestamp. | + +The current implementation primarily fills this for JamLink constraints (mono, 48kHz, 64-sample buffers). See `JamLinkHeader::fillHeaderCommonFromAudio()` in `src/PacketHeader.cpp`. + +### Empty header (`EMPTY`) + +No header; the UDP payload is raw audio data only. + +--- + +## UDP audio payload + +### Size + +For a single full packet (no redundancy), the UDP payload length is: + +$$\text{headerBytes} + (N \times C \times \text{bytesPerSample})$$ + +Where: + +- \(N\) is `BufferSize` (samples per channel) +- \(C\) is the number of channels present in the payload +- `bytesPerSample` is `BitResolution / 8` + +### Channel/sample ordering (planar / non-interleaved) + +On the wire, the payload is **planar** (non-interleaved) by channel: + +- First \(N\) samples for channel 0 +- Then \(N\) samples for channel 1 +- … + +This is explicit in `UdpDataProtocol` which converts between: + +- **Internal**: interleaved layout \([n][c]\) +- **Network**: planar layout \([c][n]\) + +See `UdpDataProtocol::sendPacketRedundancy()` and `UdpDataProtocol::receivePacketRedundancy()` in `src/UdpDataProtocol.cpp`. + +### Sample encoding (bit resolution) + +JackTrip processes audio internally as `float` (`sample_t`), but the network payload uses the selected bit resolution via `AudioInterface::fromSampleToBitConversion()` / `fromBitToSampleConversion()`. + +Behavior by bit resolution (`AudioInterface::audioBitResolutionT`): + +- **8-bit (`BIT8`)**: signed 8-bit integer, scaled from float in \([-1, 1]\). +- **16-bit (`BIT16`)**: signed 16-bit integer, written **little-endian**. +- **24-bit (`BIT24`)**: a **non-standard 3-byte format**: a 16-bit signed integer plus an 8-bit unsigned “remainder” byte. +- **32-bit (`BIT32`)**: raw 32-bit float bytes (`memcpy` of `float`), which implicitly assumes IEEE-754 and matching endianness. + +See `src/AudioInterface.cpp`. + +--- + +## UDP redundancy (optional) + +JackTrip can send redundant audio packets to reduce audible artifacts from packet loss. + +### Framing + +With redundancy factor \(R\), each UDP datagram contains **R full packets** concatenated: + +- The newest packet is first (`UDP[n]`), followed by older packets (`UDP[n-1]`, …). +- Total UDP payload length becomes `R * full_packet_size`. + +The sender implements this by shifting a buffer and prepending the newest full packet each period. + +See `UdpDataProtocol::sendPacketRedundancy()` and the explanatory comment block in `src/UdpDataProtocol.cpp`. + +### Receiver behavior + +Upon receiving a redundant datagram, the receiver: + +- Reads the first packet’s `SeqNumber`. +- If it is not the next expected sequence, scans forward through the concatenated packets looking for the expected next one. +- May “revive” and deliver multiple packets from the redundant datagram in order. +- Treats large negative or implausibly large sequence jumps as **out-of-order** and ignores them. + +See `UdpDataProtocol::receivePacketRedundancy()` in `src/UdpDataProtocol.cpp`. + +--- + +## UDP stop/control datagram + +JackTrip uses a special fixed-size UDP datagram to signal shutdown: + +- **Length**: 63 bytes +- **Contents**: every byte is `0xFF` + +The receiver checks for this exact pattern and treats it as “Peer Stopped”. + +See `UdpDataProtocol::processControlPacket()` and the shutdown path in `UdpDataProtocol::run()` in `src/UdpDataProtocol.cpp`. + +--- + +## Connection setup and “handshake” + +JackTrip supports multiple deployment styles. The relevant “protocol” differs depending on mode. + +### P2P server mode (UDP-only) + +In P2P server mode, there is **no TCP handshake**. Instead: + +- The server binds a UDP socket on its configured receive port. +- It waits for the first UDP datagram. +- It uses the datagram’s source address/port as the peer endpoint for subsequent UDP send/receive. + +This supports basic NAT traversal by responding to the client’s observed source port. + +See `JackTrip::serverStart()` and `JackTrip::receivedDataUDP()` in `src/JackTrip.cpp`. + +### Hub / ping-server mode (TCP handshake + UDP audio) + +When connecting to a hub/ping-server style endpoint, JackTrip uses a short-lived TCP connection to exchange UDP port information. + +#### Unauthenticated handshake (no TLS) + +Client → server (TCP): + +- `int32` little-endian: the client’s UDP receive/bind port +- `gMaxRemoteNameLength` bytes: optional UTF-8 “remote client name” (null-terminated, padded with zeros) + +Server → client (TCP): + +- `int32` little-endian: the server-assigned UDP port the client should use as its peer port + +The TCP connection is then closed. + +Client-side send/receive logic: `JackTrip::receivedConnectionTCP()` and `JackTrip::receivedDataTCP()` in `src/JackTrip.cpp` +Server-side receive/send logic: `UdpHubListener::readClientUdpPort()` and `UdpHubListener::sendUdpPort()` in `src/UdpHubListener.cpp` + +#### Authentication / TLS handshake (optional) + +This is an extension of the same TCP handshake using values above 65535 as “auth response” codes. + +High-level flow: + +1. Client connects TCP and sends an `int32` little-endian value of `Auth::OK` to request authentication. +2. Server replies with an `int32` auth response (e.g. `Auth::OK`, `Auth::NOTREQUIRED`, `Auth::REQUIRED`, …). +3. If both sides proceed, TLS is established on the same TCP socket. +4. Client then sends: + - `int32` LE: UDP receive/bind port + - `gMaxRemoteNameLength` bytes: client name + - `int32` LE: username length (excluding null terminator) + - `int32` LE: password length (excluding null terminator) + - `username` bytes + `\0` + - `password` bytes + `\0` +5. Server validates credentials and replies with either: + - `int32` LE UDP port (<= 65535) on success, or + - `int32` LE auth error code (> 65535) on failure + +Client-side: `JackTrip::receivedConnectionTCP()`, `JackTrip::connectionSecured()`, and `JackTrip::receivedDataTCP()` in `src/JackTrip.cpp` +Server-side: `UdpHubListener::receivedClientInfo()`, `UdpHubListener::checkAuthAndReadPort()`, and `UdpHubListener::sendUdpPort()` in `src/UdpHubListener.cpp` + +--- + +## QoS marking (best-effort) + +On supported platforms, JackTrip attempts to mark UDP packets as “voice” traffic: + +- Linux/Unix: sets DSCP to 56 (`IP_TOS` / `IPV6_TCLASS` set to `0xE0`), and sets `SO_PRIORITY` to 6. +- Windows: uses QOS APIs with `QOSTrafficTypeVoice`. +- macOS: uses `SO_NET_SERVICE_TYPE` with `NET_SERVICE_TYPE_VO` (best-effort). + +See `src/UdpDataProtocol.cpp`. + +--- + +## References + +For additional context on JackTrip's network behavior and interpretation of debug output (`-V` flag): + +Chafe, C. (2018). I am Streaming in a Room. *Frontiers in Digital Humanities*, Volume 5. https://doi.org/10.3389/fdigh.2018.00027 \ No newline at end of file diff --git a/docs/changelog.yml b/docs/changelog.yml index 1bf0351..ee60b87 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,14 @@ +- Version: "2.7.2" + Date: 2026-02-06 + Description: + - (added) Documentation for the JackTrip network protocol + - (added) Hub server - log client name with UDP port + - (fixed) Fixed crash when JACK ran out of available ports + - (fixed) Refuse to run if DYLD_INSERT_LIBRARIES is set on OSX + - (fixed) Various PLC quality improvements and bug fixes + - (fixed) Improved error message when studio connection is lost + - (fixed) Suppressed verbose logging of OSC get requests + - (fixed) Added some missing Qt dependencies to Linux docs - Version: "2.7.1" Date: 2025-06-30 Description: diff --git a/linux/Dockerfile.build b/linux/Dockerfile.build index 307b143..9d30e16 100644 --- a/linux/Dockerfile.build +++ b/linux/Dockerfile.build @@ -15,6 +15,11 @@ FROM ${BUILD_CONTAINER} AS builder # install required packages ENV DEBIAN_FRONTEND=noninteractive +RUN DEBIAN_BUSTER=$(grep buster /etc/apt/sources.list); \ + if [ -f /etc/apt/sources.list -a -n "$DEBIAN_BUSTER" ]; then \ + sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list; \ + sed -i '/buster-updates/d' /etc/apt/sources.list; \ + fi RUN apt-get update \ && apt-get install -yq --no-install-recommends curl python3-pip build-essential git libclang-dev libdbus-1-dev cmake ninja-build libjack-dev \ && apt-get install -yq --no-install-recommends libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev \ diff --git a/linux/README.md b/linux/README.md index 072397e..6e6ee33 100644 --- a/linux/README.md +++ b/linux/README.md @@ -13,7 +13,7 @@ dnf install -y qt6-qtbase qt6-qtbase-common qt6-qtbase-gui qt6-qtsvg qt6-qtwebso For Debian or Ubuntu: ``` -apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs libjack-jackd2-0 librtaudio6 libxcb-cursor0 +apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6quickdialogs2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs libjack-jackd2-0 librtaudio6 libxcb-cursor0 ``` To install JackTrip as a Linux desktop application: diff --git a/meson.build b/meson.build index f0c21fc..277dd02 100644 --- a/meson.build +++ b/meson.build @@ -370,6 +370,7 @@ if get_option('libsamplerate').allowed() opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'}) endif opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'}) + opt_var.add_cmake_defines({'CMAKE_POLICY_VERSION_MINIMUM': '3.5'}) libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var) libsamplerate_dep = libsamplerate_subproject.dependency('samplerate') found_libsamplerate = libsamplerate_dep.found() diff --git a/mkdocs.yml b/mkdocs.yml index 501de72..286f79a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - Development Tools: - Formatting: DevTools/Formatting.md - Static Analysis: DevTools/StaticAnalysis.md + - Network Protocol: Documentation/NetworkProtocol.md - Write Documentation: Documentation/MkDocs.md - About: - Contributors: About/Contributors.md diff --git a/src/JackAudioInterface.cpp b/src/JackAudioInterface.cpp index b78b385..af9ed01 100644 --- a/src/JackAudioInterface.cpp +++ b/src/JackAudioInterface.cpp @@ -185,6 +185,11 @@ void JackAudioInterface::createChannels() mInPorts[i] = jack_port_register(mClient, inName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput | JackPortIsTerminal, 0); + if (mInPorts[i] == nullptr) { + std::cerr << "*** JackAudioInterface.cpp: failed to register input port " + << inName.toStdString() << "\n"; + return; + } } // Create Output Ports @@ -195,6 +200,11 @@ void JackAudioInterface::createChannels() mOutPorts[i] = jack_port_register(mClient, outName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput | JackPortIsTerminal, 0); + if (mOutPorts[i] == nullptr) { + std::cerr << "*** JackAudioInterface.cpp: failed to register output port " + << outName.toStdString() << "\n"; + return; + } } // Create Broadcast Ports if (mBroadcast) { @@ -205,6 +215,11 @@ void JackAudioInterface::createChannels() mBroadcastPorts[i] = jack_port_register(mClient, outName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput | JackPortIsTerminal, 0); + if (mBroadcastPorts[i] == nullptr) { + std::cerr << "*** JackAudioInterface.cpp: failed to register broadcast " + << "port " << outName.toStdString() << "\n"; + return; + } } } } @@ -308,10 +323,24 @@ int JackAudioInterface::processCallback(jack_nframes_t nframes) // Input Ports are READ ONLY and change as needed (no locks) - make a copy for // debugging mInBuffer[i] = (sample_t*)jack_port_get_buffer(mInPorts[i], nframes); + if (mInBuffer[i] == nullptr) { + std::cerr + << "*** JackAudioInterface.cpp: failed to get buffer for input port " + << mInPorts[i] << " channel " << i << "/" << getNumInputChannels() + << "\n"; + return -1; + } } for (int i = 0; i < getNumOutputChannels(); i++) { // Output Ports are WRITABLE mOutBuffer[i] = (sample_t*)jack_port_get_buffer(mOutPorts[i], nframes); + if (mOutBuffer[i] == nullptr) { + std::cerr + << "*** JackAudioInterface.cpp: failed to get buffer for output port " + << mOutPorts[i] << " channel " << i << "/" << getNumOutputChannels() + << "\n"; + return -1; + } } //------------------------------------------------------------------- // TEST: Loopback diff --git a/src/OscServer.cpp b/src/OscServer.cpp index 30c94b6..2c356cc 100644 --- a/src/OscServer.cpp +++ b/src/OscServer.cpp @@ -134,7 +134,7 @@ void OscServer::handlePacket(const OSCPP::Server::Packet& packet, } } else if (msg == "/get") { const char* key = args.string(); - cout << "OSC: Get request received - key (" << key << ")" << endl; + // cout << "OSC: Get request received - key (" << key << ")" << endl; if (strcmp("latency", key) == 0) { emit signalLatencyRequested(sender, senderPort); } diff --git a/src/Regulator.cpp b/src/Regulator.cpp index 8b7cf4c..7982408 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -481,18 +481,8 @@ void Regulator::updateTolerance(int glitches, int skipped) // update headroom if (mAutoHeadroom < 0) { // variable headroom: automatically increase to minimize glitch counts - int glitchesAllowed; - if (mMsecTolerance >= (mPeerFPPdurMsec * 2)) { - // calculate glitches allowed if tolerance is above or equal to - // the duration of two packets - glitchesAllowed = std::ceil( - static_cast(AutoHeadroomGlitchTolerance * mSampleRate) / mPeerFPP); - } else { - // zero glitches allowed if tolerance is below duration of two packets - glitchesAllowed = 0; - // also don't require two intervals in a row (override) - mSkipAutoHeadroom = false; - } + const int glitchesAllowed = std::ceil( + static_cast(AutoHeadroomGlitchTolerance * mSampleRate) / mPeerFPP); // sanity check: prevent headroom from growing beyond the greater of // 3x rolling average of max, or 10ms higher than the max latency observed const int maxHeadroom = @@ -507,7 +497,8 @@ void Regulator::updateTolerance(int glitches, int skipped) if (mLastMaxLatency > mMsecTolerance + 1) { // increase headroom enough to cover any skipped packets mCurrentHeadroom = std::min( - maxHeadroom, std::ceil(mLastMaxLatency - mMsecTolerance)); + maxHeadroom, + mCurrentHeadroom + std::ceil(mLastMaxLatency - mMsecTolerance)); } else { ++mCurrentHeadroom; } @@ -630,15 +621,24 @@ bool Regulator::pullPacket() const double now = (double)mIncomingTimer.nsecsElapsed() / 1000000.0; const int lastSeqNumIn = mLastSeqNumIn.load(std::memory_order_acquire); int skipped = 0; + int firstGoodSkipped = -1; if ((lastSeqNumIn == -1) || (!mInitialized) || (now < mMsecTolerance)) { // return silence during startup: // * no packets arrived yet // * not initialized // * hasn't run long enough to meet tolerance - goto ZERO_OUTPUT; + memset(mXfrBuffer, 0, mPeerBytes); + return false; + } else if (mStashedPacket != -1) { + // use the stashed packet as the best candidate + memcpy(mXfrBuffer, mSlots[mStashedPacket], mPeerBytes); + mLastSeqNumOut = mStashedPacket; + mStashedPacket = -1; + processPacket(false); + return false; } else if (lastSeqNumIn == mLastSeqNumOut) { - goto UNDERRUN; + return underrun(now, lastSeqNumIn); } else { // calculate how many new packets we want to look at to // find the next packet to pull @@ -670,46 +670,50 @@ bool Regulator::pullPacket() } // check if packet's age matches tolerance, or is the best candidate we have if (mIncomingTiming[next] + mMsecTolerance >= now || i == 0) { + if (skipped == 1 && firstGoodSkipped >= 0) { + // special case where we are about to skip 1 good packet. + // this defers latency adjustments until they are at least + // 2 packets wide. + mStashedPacket = next; + next = firstGoodSkipped; + } else if (skipped > 0) { + // process a glitch to account for the skipped packets, + // but stash and use this good packet on next callback. + pullStat->plcOverruns += skipped; + mSkipped += skipped; + mStashedPacket = next; + processPacket(true); + return true; + } // next is the best candidate memcpy(mXfrBuffer, mSlots[next], mPeerBytes); mLastSeqNumOut = next; - goto PACKETOK; + processPacket(false); + return false; } - ++mSkipped; + if (firstGoodSkipped == -1) + firstGoodSkipped = next; } - - // no viable candidate - goto UNDERRUN; } -PACKETOK : { - pullStat->plcOverruns += skipped; - if (skipped && !mLastWasGlitch) { - processPacket(true); - return true; - } else - processPacket(false); - goto OUTPUT; -} + // no viable candidate + return underrun(now, lastSeqNumIn); +}; -UNDERRUN : { +//******************************************************************************* +bool Regulator::underrun(const double now, const int lastSeqNumIn) +{ pullStat->plcUnderruns++; // count late if ((mLastSeqNumOut == lastSeqNumIn) && ((now - mIncomingTiming[mLastSeqNumOut]) > gUdpWaitTimeout)) { - goto ZERO_OUTPUT; + memset(mXfrBuffer, 0, mPeerBytes); + return false; } // "good underrun", not a stuck client processPacket(true); return true; } -ZERO_OUTPUT: - memset(mXfrBuffer, 0, mPeerBytes); - -OUTPUT: - return false; -}; - //******************************************************************************* void Regulator::processPacket(bool glitch) { diff --git a/src/Regulator.h b/src/Regulator.h index a4a4b50..6041cdd 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -236,7 +236,8 @@ class Regulator : public RingBuffer private: void pushPacket(const int8_t* buf, int seq_num); void updatePushStats(int seq_num); - bool pullPacket(); // returns true if PLC prediction + bool pullPacket(); // returns true if PLC prediction + bool underrun(const double now, const int lastSeqNumIn); bool enableWorker(); // returns true if worker was enabled void updateTolerance(int glitches, int skipped); void setFPPratio(int len); @@ -288,6 +289,7 @@ class Regulator : public RingBuffer StdDev* pullStat = nullptr; double mMsecTolerance = 64; int mLastSeqNumOut = -1; + int mStashedPacket = -1; std::atomic mLastSeqNumIn; QElapsedTimer mIncomingTimer; std::vector mIncomingTiming; diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp index e42f6ad..9006d76 100644 --- a/src/UdpHubListener.cpp +++ b/src/UdpHubListener.cpp @@ -325,7 +325,8 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection) // Assign server port and send it to Client if (id != -1) { cout << "JackTrip HUB SERVER: Sending Final UDP Port to Client: " - << mJTWorkers->at(id)->getServerPort() << endl; + << clientName.toStdString() << " = " << mJTWorkers->at(id)->getServerPort() + << endl; } if (id == -1 diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index d11dddc..3a56e5d 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "jacktrip_types.h" -constexpr const char* const gVersion = "2.7.1"; ///< JackTrip version +constexpr const char* const gVersion = "2.7.2"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/main.cpp b/src/main.cpp index 8ef5908..1310259 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,6 +57,20 @@ QCoreApplication* createApplication(int& argc, char* argv[]) { +#ifdef __APPLE__ + // Check for the DYLD_INSERT_LIBRARIES environment variable. + // Refuse to run if it is set, to avoid code injection attacks. + // Just an extra precaution since QtWebEngine requires the entitlement + // com.apple.security.cs.allow-dyld-environment-variable + // See https://doc.qt.io/qt-6/qtwebengine-deploying.html + if (getenv("DYLD_INSERT_LIBRARIES") != nullptr) { + std::cout << "Detected environment variable: DYLD_INSERT_LIBRARIES." << std::endl; + std::cout << "To run JackTrip, please omit the this environment variable." + << std::endl; + std::exit(1); + } +#endif + // Check for some specific, GUI related command line options. bool forceGui = false; bool testGui = false; diff --git a/src/vs/virtualstudio.cpp b/src/vs/virtualstudio.cpp index d685645..3a81585 100644 --- a/src/vs/virtualstudio.cpp +++ b/src/vs/virtualstudio.cpp @@ -1478,7 +1478,7 @@ void VirtualStudio::processError(const QString& errorMessage) msgBox.setWindowTitle(QStringLiteral("No JACK server")); } else if (errorMessage == QLatin1String("Peer Stopped")) { // Report the other end quitting as a regular occurance rather than an error. - msgBox.setText("The Studio has been stopped."); + msgBox.setText("Lost connection to the studio."); msgBox.setWindowTitle(QStringLiteral("Disconnected")); } else if (errorMessage.startsWith(RtAudioErrorMsg)) { if (errorMessage.length() > RtAudioErrorMsg.length() + 2) { -- 2.30.2